/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14.exp;

import edu.uoc.sd.fr14.Configuration;
import edu.uoc.sd.fr14.mesg.ExperimentMessage;
import edu.uoc.sd.fr14.mesg.Message;
import edu.uoc.sd.fr14.mesg.TargetId;
import edu.uoc.sd.fr14.model.Logger;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 *
 * @author drodenas
 */
public abstract class Experiment {
    
    private static final Logger log = Logger.get("Experiment");
    private final List<Message> outputQueue = new LinkedList<>();
    private final Map<TargetId,Map<String,Object>> states = new TreeMap<>();

    private final TargetId me;
    private final Set<TargetId> mes;
    private final Set<TargetId> others;
    private final ResultCollector resultCollector;

    @SuppressWarnings({ "rawtypes", "unchecked" })
	public Experiment(Set mes, Configuration config, ResultCollector resultCollector) {
        this.me = (TargetId) mes.iterator().next();
        this.mes = new LinkedHashSet<>(mes);
        this.resultCollector = resultCollector;
                
        others = new LinkedHashSet<>();
        others.addAll(config.servers());
        others.add(config.customers().iterator().next());
        others.removeAll(mes);
        
        states.put(me, new TreeMap<String,Object>());
    }
    
    protected final void collectResult(boolean success, String result) {
    	resultCollector.collect(success, result);
    }
    
    protected final Map<TargetId, Map<String, Object>> getStates() {
		return states;
	}
    
    protected final String getString(String key) {
        return (String) states.get(me).get(key);
    }
    
    protected final Boolean getBoolean(String key) {
        return (Boolean) states.get(me).get(key);
    }
    
    protected final void saveBoolean(String key, boolean value) {
        saveObject(key, value);
    }
    
    protected final void saveString(String key, String value) {
        saveObject(key, value);
    }
    
    protected void saveObject(String key, Serializable value) {
        states.get(me).put(key, value);        
    }
    
    protected boolean othersEqual(String key, Object expected) {
        for (TargetId target : others) {
            Map<String,Object> state = states.get(target);
            if (state == null) {
                return false;
            }
            Object value = state.get(key);
            if (value == null) {
                return false;
            }
            if (!value.equals(expected)) {
                return false;
            }
        }
        return true;
    }
    
    protected boolean othersEqual(String key, Object expected, Object alternative) {
        for (TargetId target : others) {
            Map<String,Object> state = states.get(target);
            if (state == null) {
                return false;
            }
            Object value = state.get(key);
            if (value == null) {
                return false;
            }
            if (!value.equals(expected) && !value.equals(alternative)) {
                return false;
            }
        }
        return true;
    }

	protected void dumpState() {
		for (Map.Entry<TargetId,Map<String,Object>> kkEclipse : states.entrySet()) {
			TargetId server = kkEclipse.getKey();
			Map<String,Object> values = kkEclipse.getValue();
			System.out.println("# "+server+" ==================================");
			for (Map.Entry<String,Object> entry : values.entrySet()) {
				System.out.println("#="+entry.getKey() +"="+ entry.getValue());
			}
			System.out.println("###############################################");
		}
		System.out.println(states);
	}
    
    public void apply(Message message) {
        if (message instanceof ExperimentMessage) {
            ExperimentMessage experimentMessage = (ExperimentMessage) message;
            TargetId from = experimentMessage.getFrom();
            Map<String,Object> state = experimentMessage.getState();
            if (others.contains(from) && isChanged(from, state)) {
                states.put(from, state);
                log.debug(me, "apply('"+message+"') states:"+states);
            }
        }
    }

    public abstract void tick();
    
    protected void putOutput(Message message) {
        outputQueue.add(message);
    }
    
    public List<Message> flushOutput() {
        Map<String,Object> state = new TreeMap<>(states.get(me));
        for (TargetId from : mes) {
            for (TargetId target : others) {
                outputQueue.add(new ExperimentMessage(target, from, state));
            }
        }
        
        List<Message> result = new ArrayList<>(outputQueue);
        outputQueue.clear();
        return result;
    }
    
    
    private boolean isChanged(TargetId target, Map<String,Object> other) {
        Map<String,Object> known = states.get(target);
        if (known == null) return true;
        
        for (Map.Entry<String, Object> entry : other.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            Object mine = known.get(key);
            
            if (mine == null) {
                return true;
            }
            
            if (!value.equals(mine)) {
                return true;
            }
        }
        
        return false;
    }
    
    public static interface ResultCollector {
    	void collect(boolean success, String result);
    }
    
    
}
